/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.datastore.guava

import android.content.Context
import androidx.concurrent.futures.SuspendToFutureAdapter.launchFuture
import androidx.datastore.core.DataMigration
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.Serializer
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import com.google.common.util.concurrent.ListenableFuture
import java.io.File
import java.util.concurrent.Callable
import java.util.concurrent.Executor
import java.util.function.Function
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.flow.first

/**
 * The class that wraps around [DataStore] to provide an interface that returns [ListenableFuture]s
 * for DataStore reads and writes.
 */
public class GuavaDataStore<T : Any>
internal constructor(
    /** The delegate DataStore. */
    private val dataStore: DataStore<T>,
    /** The [CoroutineContext] that holds a dispatcher. */
    private val coroutineContext: CoroutineContext,
) {
    /**
     * Returns a [ListenableFuture] to get the latest persisted data. It is not blocked by any
     * ongoing updates.
     */
    public fun getDataAsync(): ListenableFuture<T> {
        return launchFuture(coroutineContext) { dataStore.data.first() }
    }

    /**
     * Returns a [ListenableFuture] to update the data using the provided [transform]. The
     * [transform] is given the latest persisted data to produce its output, which is then persisted
     * and returned. Concurrent updates are serialized (at most one update running at a time).
     */
    public fun updateDataAsync(transform: (input: T) -> T): ListenableFuture<T> {
        return launchFuture(coroutineContext) { dataStore.updateData { transform(it) } }
    }

    /** Builder class for a [GuavaDataStore] that works on a single process. */
    public class Builder<T : Any>(
        /**
         * Create a [GuavaDataStoreBuilder] with the [Callable] which returns the File that
         * [DataStore] acts on. The user is responsible for ensuring that there is never more than
         * one [DataStore] acting on a file at a time.
         *
         * @param serializer the [Serializer] for the type that this DataStore acts on.
         * @param produceFile [Function] which returns the file that the new [DataStore] will act
         *   on. The function must return the same path every time. No two instances of DataStore
         *   should act on the same file at the same time.
         */
        private val serializer: Serializer<T>,
        private val produceFile: Callable<File>,
    ) {

        /**
         * Create a [GuavaDataStoreBuilder] with the [Context] and name from which to derive the
         * [DataStore] file. The file is generated by File(this.filesDir, "datastore/$fileName").
         * The user is responsible for ensuring that there is never more than one DataStore acting
         * on a file at a time.
         *
         * Either produceFile or context & name must be set, but not both. This is enforced by the
         * two constructors.
         *
         * @param context the [Context] from which we retrieve files directory.
         * @param fileName the filename relative to [Context.applicationContext.filesDir] that
         *   [DataStore] acts on. The File is obtained from [dataStoreFile]. It is created in the
         *   "/datastore" subdirectory.
         * @param serializer the [Serializer] for the type that this DataStore acts on.
         */
        public constructor(
            context: Context,
            fileName: String,
            serializer: Serializer<T>
        ) : this(serializer, produceFile = { context.dataStoreFile(fileName) })

        // Optional
        private var corruptionHandler: ReplaceFileCorruptionHandler<T>? = null
        private val dataMigrations: MutableList<DataMigration<T>> = mutableListOf()
        private var coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO

        /**
         * Sets the corruption handler to install into the DataStore.
         *
         * This parameter is optional and defaults to no corruption handler.
         *
         * @param corruptionHandler the handler to invoke when there is a file corruption
         * @return this
         */
        @Suppress("MissingGetterMatchingBuilder")
        public fun setCorruptionHandler(
            corruptionHandler: ReplaceFileCorruptionHandler<T>
        ): Builder<T> = apply { this.corruptionHandler = corruptionHandler }

        /**
         * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
         *
         * @param dataMigration the migration to add
         * @return this
         */
        @Suppress("MissingGetterMatchingBuilder")
        public fun addDataMigration(dataMigration: DataMigration<T>): Builder<T> = apply {
            this.dataMigrations.add(dataMigration)
        }

        /**
         * Sets the [Executor] used by the DataStore.
         *
         * This parameter is optional and defaults to [Dispatchers.IO].
         *
         * @param executor the executor to be used by [DataStore]
         * @return this
         */
        @Suppress("MissingGetterMatchingBuilder")
        public fun setExecutor(executor: Executor): Builder<T> = apply {
            this.coroutineDispatcher = executor.asCoroutineDispatcher()
        }

        /**
         * Build the DataStore.
         *
         * @return the DataStore with the provided parameters
         */
        public fun build(): GuavaDataStore<T> =
            GuavaDataStore(
                DataStoreFactory.create(
                    produceFile = produceFile::call,
                    serializer = serializer,
                    corruptionHandler = corruptionHandler,
                    migrations = dataMigrations,
                    scope = CoroutineScope(coroutineDispatcher),
                ),
                coroutineDispatcher,
            )
    }
}
